﻿using System;
using System.Diagnostics;
using System.IO;
using MLSharp.Classification;

namespace MLSharp.See5
{
	/// <summary>
	/// An implementation of <see cref="IDecisionTree"/>
	/// that is used by <see cref="See5"/>.
	/// </summary>
	internal class See5DecisionTree : IDecisionTree, IDisposable
	{
		#region Private Fields

		/// <summary>
		/// The See5 stem file name
		/// </summary>
		private readonly string mStemName;

		/// <summary>
		/// The name of the attribute to predict
		/// </summary>
		private readonly string mTargetAttributeName;

		/// <summary>
		/// The path to the See5 "See5Sam.exe" program
		/// </summary>
		private readonly string mSee5SamPath;

		/// <summary>
		/// The working directory to use.
		/// </summary>
		private readonly string mSee5WorkingDirectory;

		/// <summary>
		/// Flag that tracks whether or not Dispose has been called on this object.
		/// </summary>
		private bool mDisposed;

		#endregion

		#region Internal Properties

		/// <summary>
		/// Returns the path to the tree file.
		/// </summary>
		/// <remarks>
		/// Right now, this is only used for unit testing.
		/// </remarks>
		internal string TreeFile
		{
			get
			{
				return Path.Combine(mSee5WorkingDirectory, mStemName + ".tree");
			}
		}

		#endregion

		#region Public Constructors

		/// <summary>
		/// Creates a new instance of the decision tree bound to the file
		/// at the specified path.
		/// </summary>
		/// <param name="stemName"></param>
		/// <param name="targetAttributeName"></param>
		/// <param name="see5WorkingDirectory"></param>
		/// <param name="see5SamPath"></param>
		public See5DecisionTree(string stemName, string targetAttributeName, string see5SamPath, string see5WorkingDirectory)
		{
			mStemName = stemName;
			mTargetAttributeName = targetAttributeName;
			mSee5SamPath = see5SamPath;
			mSee5WorkingDirectory = see5WorkingDirectory;
		}

		#endregion

		#region Public Methods

		/// <summary>
		/// Runs the decision tree against the specified data set.
		/// </summary>
		/// <param name="dataSet">The data set.</param>
		/// <returns>An array of results, one for each instance in the data set.</returns>
		/// <remarks>
		/// Be sure the schema of the data set is *exactly* the same as the data set
		/// that was used to construct this tree, otherwise the behavior may be 
		/// incorrect.
		/// </remarks>
		public ClassificationResult[] Classify(IDataSet dataSet)
		{
			//Step 1: Write the data set to the See 5 working directory
			See5DataWriter writer = new See5DataWriter(mStemName, mTargetAttributeName);
			writer.WriteCases(dataSet, mSee5WorkingDirectory);

			//Prepare to run See5Sam in that directory.
			ProcessStartInfo see5Info = new ProcessStartInfo();
			see5Info.FileName = mSee5SamPath;
			see5Info.WorkingDirectory = mSee5WorkingDirectory;
			see5Info.Arguments = string.Format("-f {0}", mStemName);
			see5Info.UseShellExecute = false;
			see5Info.RedirectStandardOutput = true;

			//Step 2: Now that everything is in place and configured, run See5...
			Process see5Process = Process.Start(see5Info);

			//Step 3: Process the output from it and build the results
			//The first 3 lines are just headers, so skip them.
			see5Process.StandardOutput.ReadLine();
			see5Process.StandardOutput.ReadLine();
			see5Process.StandardOutput.ReadLine();

			ClassificationResult[] results = new ClassificationResult[dataSet.Instances.Count];
			int count = 0;

			//Now loop through and process each case.
			while (!see5Process.StandardOutput.EndOfStream)
			{
				string output = see5Process.StandardOutput.ReadLine();

				//A row of output looks like:
				//{id}    {given label}    {predicted label}   [{confidence}]
				string[] values = output.Split(new[] { " ", "\t" }, StringSplitOptions.RemoveEmptyEntries);

				ClassificationResult result = new ClassificationResult(values[2], dataSet.Instances[count].ClassValue)
				                              	{
				                              		ID = values[0],
				                              		PredictedClass = values[2],
				                              		Confidence = double.Parse(values[3].Trim('[', ']'))
				                              	};

				results[count++] = result;
			}

			see5Process.WaitForExit();

			//Clean up the .cases file
			File.Delete(Path.Combine(mSee5WorkingDirectory, mStemName + ".cases"));

			return results;
		}

		#endregion

		#region Finalization/IDisposable-related Methods

		/// <summary>
		/// Cleans up the temporary files used by the tree.
		/// </summary>
		public void Dispose()
		{
			if (!mDisposed)
			{
				//Remove all temporary files related to this tree: a .names, a .tmp, and a .tree file.
				foreach (string extension in new string [] { ".names", ".tmp", ".tree"})
				{
					string target = Path.Combine(mSee5WorkingDirectory, mStemName + extension);

					if (File.Exists(target))
					{
						File.Delete(target);
					}
				}
			}

			mDisposed = true;

			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Finalizer that insures everything has been cleaned up.
		/// </summary>
		~See5DecisionTree()
		{
			Dispose();
		}

		#endregion
	}
}
